#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_ttf.h>
#include <SDL2/SDL_mixer.h>
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include "ltimer.h"
#include "global.h"

/* 绍音频编程录制和回放的一些基础知识
声音是一种波。波可以用一串值来表示（在这首立体声歌曲中，则是两个序列，每个声波一个）。播放音频只是向音频驱动程序发送一串数值，而录制音频则是从音频驱动程序复制一串数值
*/

//全部面积大小
// 由于渲染的大小不再是屏幕的大小，必须有一组单独的常量来定义渲染的大小。
const int LEVEL_WIDTH = 1280;
const int LEVEL_HEIGHT = 960;

//当前框大小
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
// const int SCREEN_FPS = 50;
// const int SCREEN_TICKS_PER_FRAME = 1000 / SCREEN_FPS;
//Number of data integers
const int TOTAL_DATA = 10;

//声明一个长度为 32 位的有符号整数数组。这将是要加载和保存的数据。在本演示中，这个数组的长度为 10
Sint32 gData[ TOTAL_DATA ];

// 渲染的窗口
SDL_Window * gWindow = nullptr;

// 当我们处理 SDL 纹理时，需要一个 SDL_Renderer 将其渲染到屏幕上
//The window renderer
SDL_Renderer* gRenderer = nullptr;

TTF_Font* gFont = nullptr; 

//支持的记录设备最大数量
const int MAX_RECORDING_DEVICES = 10;

//最长记录时间
const int MAX_RECORDING_SECONDS = 5;

//最长记录时间加填充  为了防止应用程序录制 5.1 秒左右的内容，允许录制 6 秒钟
const int RECORDING_BUFFER_SECONDS = MAX_RECORDING_SECONDS + 1;

// 这两个回调将进行实际录音，并从音频缓冲区播放
void audioRecordingCallback( void* userdata, Uint8* stream, int len );
void audioPlaybackCallback( void* userdata, Uint8* stream, int len );

//枚举各种记录状态
enum RecordingState
{
    SELECTING_DEVICE,
    STOPPED,
    RECORDING,
    RECORDED,
    PLAYBACK,
    ERROR
};

// 封装SDL_Texture
//Texture wrapper class
class LTexture
{
    public:
        //Initializes variables
        LTexture();

        //Deallocates memory
        ~LTexture();

        //Loads image at specified path
        bool loadFromFile( std::string path );

        #if defined(SDL_TTF_MAJOR_VERSION)
        /*
        如果头文件不包含 SDL_ttf.h，编译器就会忽略它。它检查是否定义了 SDL_TTF_MAJOR_VERSION 宏。
        与 #include 一样，#if 也是用于与编译器对话的宏。在本例中，它表示如果 SDL_ttf 未定义，则忽略这段代码
        */
        //Creates image from font string
        // SDL_ttf 的工作方式是根据字体和颜色创建新图像。对于纹理类来说，这意味着将从 SDL_ttf 渲染的文本而不是文件中加载图像
        bool loadFromRenderedText( std::string textureText, SDL_Color textColor );
        #endif

        //Deallocates texture
        void free();

        //设置调制颜色，接收红、绿、蓝三色分量
        void setColor( Uint8 red, Uint8 green, Uint8 blue );

        //设置纹理混合  它将控制纹理的混合方式。为了使混合正常工作，必须设置纹理的混合模式
        void setBlendMode( SDL_BlendMode blending );

        //设置纹理的 alpha 透明度
        void setAlpha( Uint8 alpha );

        //Renders texture at given point
        /*接受一个矩形参数，用来定义我们要渲染的纹理部分。我们给它一个默认参数 nullptr，以防我们想要渲染整个纹理
          接受旋转角度、纹理旋转点和 SDL 翻转枚举值，同样也给出了参数的默认值，以备在不旋转或翻转的情况下渲染纹理。
        */
        void render( int x, int y, SDL_Rect* clip = nullptr, double angle = 0.0, SDL_Point* center = nullptr, SDL_RendererFlip flip = SDL_FLIP_NONE );

        //Gets image dimensions
        int getWidth();
        int getHeight();

    private:
        //实际渲染的纹理
        SDL_Texture* mTexture;

        //图像的宽和高
        int mWidth;
        int mHeight;
};

LTexture::LTexture()
{
    //Initialize
    mTexture = nullptr;
    mWidth = 0;
    mHeight = 0;
}

LTexture::~LTexture()
{
    //Deallocate
    free();
}

void LTexture::setColor( Uint8 red, Uint8 green, Uint8 blue )
{
    //纹理颜色调制
    /*
    DL_SetTextureColorMod 接受 Uint8 作为颜色组件的参数。Uint8 只是一个无符号的 8 位整数。这意味着它的范围从 0 到 255。
    128 大约介于 0 和 255 之间，因此当您将绿色调制为 128 时，纹理上任何像素的绿色分量都会减半。
    红色和蓝色方格不会受到影响，因为它们没有绿色，但绿色的亮度会减半，白色则会变成浅洋红色（洋红色为红色 255、绿色 0、蓝色 255）。
    颜色调制只是将一种颜色乘以整个纹理的一种方法
    */
    SDL_SetTextureColorMod( mTexture, red, green, blue );
}

#if defined(SDL_TTF_MAJOR_VERSION)
bool LTexture::loadFromRenderedText( std::string textureText, SDL_Color textColor )
{
    ////先取消原有纹理的渲染
    free();

    //Render text surface
    /*
    根据字体创建要渲染的纹理。该函数接收要渲染的文本字符串和要用来渲染它的颜色。在此之后，该函数的工作原理与从文件加载相似，
    只不过这次使用的是由 SDL_ttf 创建的 SDL_Surface，而不是文件
    使用 TTF_RenderText_Solid 加载一个曲面。函数会根据给出的字体、文本和颜色创建一个纯色曲面。如果曲面创建成功，
    就会像之前从文件加载曲面一样创建纹理。创建纹理后，我们就可以像渲染其他纹理一样对其进行渲染。
    */
    if(gFont == nullptr)
    {
        std::cout << "gFont nullptr" << std::endl;
    }
    SDL_Surface* textSurface = TTF_RenderText_Solid( gFont, textureText.c_str(), textColor );
    if( textSurface == nullptr )
    {
        std::cout << "Unable to render text surface! SDL_ttf Error:" << TTF_GetError() << std::endl;
    }
    else
    {
        //Create texture from surface pixels
        mTexture = SDL_CreateTextureFromSurface( gRenderer, textSurface );
        if( mTexture == nullptr )
        {
            std::cout << "Unable to create texture from rendered text! SDL Error:" << SDL_GetError() << std::endl;
        }
        else
        {
            //Get image dimensions
            mWidth = textSurface->w;
            mHeight = textSurface->h;
        }

        //Get rid of old surface
        SDL_FreeSurface( textSurface );
    }
    
    //Return success
    return mTexture != NULL;
}
#endif

bool LTexture::loadFromFile( std::string path )
{
    //先取消原有纹理的渲染
    free();

    //The final texture
    SDL_Texture* newTexture = nullptr;

    //Load image at specified path
    SDL_Surface* loadedSurface = IMG_Load( path.c_str() );
    if( loadedSurface == nullptr )
    {
        std::cout<< "Unable to load image "<< path.c_str() << "!, SDL_image Error: " << IMG_GetError() << std::endl;
    }
    else
    {
        /*  使用 SDL_SetColorKey 对图像进行去除背景。第一个参数是我们要进行去除背景处理的表面，第二个参数是我们是否要启用去除背景处理，
        最后一个参数是我们要进行去除背景处理的像素(图像中哪种颜色要去除)
            从 RGB 颜色创建像素的方法是使用 SDL_MapRGB。第一个参数是我们想要的像素格式(SDL_PixelFormat 结构)。幸运的是，载入的曲面中有一个成员变量可以获取该格式。
         最后三个变量是要映射的颜色的红、绿、蓝分量。
         青色 0，255，255
         黑色 0，0，0
         白色 255，255，255
        */
        SDL_SetColorKey( loadedSurface, SDL_TRUE, SDL_MapRGB( loadedSurface->format, 0, 0xff, 0xff ) );

        //Create texture from surface pixels
        newTexture = SDL_CreateTextureFromSurface( gRenderer, loadedSurface );
        if( newTexture == nullptr )
        {
            std::cout << "Unable to create texture from " << path.c_str() << "! SDL Error: " << SDL_GetError() << std::endl;
        }
        else
        {
            //Get image dimensions
            mWidth = loadedSurface->w;
            mHeight = loadedSurface->h;
        }

        //清楚旧纹理加载
        SDL_FreeSurface( loadedSurface );
    }

    //Return success
    mTexture = newTexture;
    return mTexture != nullptr;
}

void LTexture::free()
{
    //Free texture if it exists
    if( mTexture != nullptr )
    {
        SDL_DestroyTexture( mTexture );
        mTexture = nullptr;
        mWidth = 0;
        mHeight = 0;
    }
}

void LTexture::render( int x, int y, SDL_Rect* clip, double angle, SDL_Point* center, SDL_RendererFlip flip )
{
    //设置渲染区域并渲染到屏幕
    SDL_Rect renderQuad = { x, y, mWidth, mHeight };
    /* 在特定位置渲染纹理时，需要指定一个目标矩形，该矩形可设置 x/y 位置和宽度/高度。在不知道原始图像尺寸的情况下，我们无法指定宽度/高度。
    因此，当我们渲染纹理时，我们会创建一个包含位置参数和成员宽度/高度的矩形，并将此矩形传递给 SDL_RenderCopy
    */

    //Set clip rendering dimensions
    if( clip != nullptr )
    {
        renderQuad.w = clip->w;
        renderQuad.h = clip->h;
    }
    /*
    在剪辑时，如果使用的是剪辑矩形的尺寸而不是纹理的尺寸，我们将把目标矩形（此处称为 renderQuad）的宽度/高度设置为剪辑矩形的尺寸。
    我们要将剪辑矩形作为源矩形传递给 SDL_RenderCopy。源矩形定义了要渲染纹理的哪一部分。当源矩形为空时，将渲染整个纹理。
    */

    //Render to screen
    // SDL_RenderCopy( gRenderer, mTexture, clip, &renderQuad );
    //Render to screen 该函数的工作原理与原始的 SDL_RenderCopy 相同，但增加了用于旋转和翻转的参数
    SDL_RenderCopyEx( gRenderer, mTexture, clip, &renderQuad, angle, center, flip );
}

int LTexture::getWidth()
{
    return mWidth;
}

int LTexture::getHeight()
{
    return mHeight;
}

void LTexture::setBlendMode( SDL_BlendMode blending )
{
    //Set blending function
    // SDL_SetTextureBlendMode 允许启用混合模式
    SDL_SetTextureBlendMode( mTexture, blending );
}
        
void LTexture::setAlpha( Uint8 alpha )
{
    //Modulate texture alpha
    // SDL_SetTextureAlphaMod 则允许设置整个纹理的 Alpha 值
    SDL_SetTextureAlphaMod( mTexture, alpha );
}


LTexture gDotTexture;
// LTexture gPromptTextTexture;
// LTexture gDataTextures[TOTAL_DATA];

//用于提示用户，了解现在状态
LTexture gPromptTexture;

//纹理数组用于存储录音设备的名称
LTexture gDeviceTextures[ MAX_RECORDING_DEVICES ];

//记录可用设备的数量
int gRecordingDeviceCount = 0;

//Received audio spec
/*SDL_AudioSpec 是一种音频规范，基本上定义了如何录制或播放音频。当打开一个音频设备进行录制或播放时，会请求一个规范，
但由于音频驱动程序不支持该规范，可能无法得到所请求的结果。这就是为什么要存储从驱动程序获取的用于录音和播放的规范。
*/
SDL_AudioSpec gReceivedRecordingSpec;
SDL_AudioSpec gReceivedPlaybackSpec;

// "gBufferByteSize "是 6 秒字节（5 秒 + 1 秒填充），而 "gBufferByteMaxPosition "是使用的 5 秒字节
//一个由无符号字节组成的缓冲区，用于存储音频数据
Uint8* gRecordingBuffer = nullptr;

//存储缓冲区的字节数
Uint32 gBufferByteSize = 0;

//控制录制或重放时在缓冲区中的位置
Uint32 gBufferBytePosition = 0;

//(缓冲区字节最大位置）控制使用的缓冲区的最大片段
Uint32 gBufferByteMaxPosition = 0;

//Render text r,g,b,a
SDL_Color textColor = { 0, 0xff, 0, 0xff };


//点类，可以在屏幕上移动
class Dot
{
    public:
        //点的尺寸
        static const int DOT_WIDTH = 20;
        static const int DOT_HEIGHT = 20;

        //点的最大轴速度
        static const int DOT_VEL = 10;

        //Initializes the variables
        Dot(int x, int y);

        Dot();

        //按下按键并调整点的速度
        void handleEvent( SDL_Event& e );

        //移动点并检查碰撞 移动函数将接收一个碰撞方框组，这样就可以对两组碰撞方框进行检查
        void move(std::vector<SDL_Rect>& otherColliders);

        //Moves the dot and checks collision
        void move( SDL_Rect& square, Circle& circle );

        void move();

        //在屏幕上渲染点
        void render();

        // 渲染屏幕上相对于当前框的点
        // 点必须相对于当前框进行渲染，因此它的渲染函数接收了当前框的位置
        void render( int camX, int camY );

        //获取每个点的碰撞方框组
        std::vector<SDL_Rect>& getColliders();

         //Gets collision circle
        Circle& getCollider();

        //Position accessors
        int getPosX();
        int getPosY();

    private:
        //点的坐标
        int mPosX, mPosY;

        //点的速度
        int mVelX, mVelY;

        //点的碰撞方框组
        std::vector<SDL_Rect> mColliders;

        //Dot's collision circle
        Circle mCollider;

        //根据点的坐标移动碰撞框组
        void shiftColliders();
};

int Dot::getPosX()
{
    return mPosX;
}

int Dot::getPosY()
{
    return mPosY;
}

Dot::Dot(int x, int y)
{
    //Initialize the offsets
    mPosX = x;
    mPosY = y;

    //Set collision circle size
    mCollider.r = DOT_WIDTH / 2;

    //Initialize the velocity
    mVelX = 0;
    mVelY = 0;

    //Move collider relative to the circle
    shiftColliders();
}

void Dot::handleEvent( SDL_Event& e )
{
    //If a key was pressed
    // 如果每次按右键时都增加 x 位置，那么就必须反复按右键才能保持移动。而通过设置速度，只需按一次键即可
    //要检查按键重复次数是否为 0，因为按键重复次数默认是启用的，如果按住一个键，它会报告多次按键。这意味着必须检查按键是否是第一次按下，因为只关心按键第一次按下的时间
    if( e.type == SDL_KEYDOWN && e.key.repeat == 0 )
    {
        std::cout << "key press" << std::endl;
        //Adjust the velocity
        switch( e.key.keysym.sym )
        {
            // 如果一个物体以每帧 10 像素的速度向右移动，它的速度就是 10。如果它以每帧 10 像素的速度向左移动，则速度为-10。如果点的速度是 10，这意味着 10 帧后它将移动 100 个像素
            case SDLK_UP: mVelY -= DOT_VEL; break;
            case SDLK_DOWN: mVelY += DOT_VEL; break;
            case SDLK_LEFT: mVelX -= DOT_VEL; break;
            case SDLK_RIGHT: mVelX += DOT_VEL; break;
        }
    }

    //If a key was released
    else if( e.type == SDL_KEYUP && e.key.repeat == 0 )
    {
        std::cout << "key release" << std::endl;
        //Adjust the velocity
        switch( e.key.keysym.sym )
        {
            case SDLK_UP: mVelY += DOT_VEL; break;
            case SDLK_DOWN: mVelY -= DOT_VEL; break;
            case SDLK_LEFT: mVelX += DOT_VEL; break;
            case SDLK_RIGHT: mVelX -= DOT_VEL; break;
        }
    }
}

Dot::Dot()
{
    //Initialize the offsets
    mPosX = SCREEN_WIDTH / 2;
    mPosY = SCREEN_HEIGHT / 2;

    //Initialize the velocity
    mVelX = 0;
    mVelY = 0;
}

// 每帧移动圆点时调用的函数
void Dot::move(std::vector<SDL_Rect>& otherColliders)
{
    //Move the dot left or right
    mPosX += mVelX;
    shiftColliders();
    std::cout << "dot move" << ", mPosX=" << mPosX << std::endl;

    /*
    沿 x 轴移动圆点，同时也改变碰撞器的位置。每当改变点的位置时，碰撞器的位置也必须跟着改变。
    然后，检查点是否离开屏幕或撞到墙壁。如果有，就将点沿 x 轴向后移动。最后，再对 Y 轴上的运动做一次这样的检查。
    */
    //若点相撞或过于偏左或偏右
    if( ( mPosX < 0 ) || ( mPosX + DOT_WIDTH > SCREEN_WIDTH ) || checkCollision( this->mColliders, otherColliders ) )
    {
        //Move back
        mPosX -= mVelX;
        shiftColliders();
    }

    //Move the dot up or down
    mPosY += mVelY;
    shiftColliders();
    std::cout << "dot move" << ", mPosY=" << mPosY << std::endl;

    //若点相撞或过于偏上或偏下
    if( ( mPosY < 0 ) || ( mPosY + DOT_HEIGHT > SCREEN_HEIGHT ) || checkCollision( this->mColliders, otherColliders )  )
    {
        //Move back
        mPosY -= mVelY;
        shiftColliders();
    }
}

void Dot::move( SDL_Rect& square, Circle& circle )
{
    //Move the dot left or right
    mPosX += mVelX;
    shiftColliders();
    std::cout << "dot move" << ", mPosX=" << mPosX << ",mVelX=" << mVelX << std::endl;
    bool temp1 = checkCollision( mCollider, square );
    bool temp2 = checkCollision( mCollider, circle );
    std::cout << "dot move" << ", temp1=" << temp1 << ",temp2=" << temp2 << std::endl;

    //If the dot collided or went too far to the left or right
    if( ( mPosX - mCollider.r < 0 ) || ( mPosX + mCollider.r > SCREEN_WIDTH ) || checkCollision( mCollider, square ) || checkCollision( mCollider, circle ) )
    {
        //Move back
        mPosX -= mVelX;
        shiftColliders();
    }

    //Move the dot up or down
    mPosY += mVelY;
    shiftColliders();
    std::cout << "dot move" << ", mPosY=" << mPosY << ",mVelY=" << mVelY << std::endl;

    //If the dot collided or went too far up or down
    if( ( mPosY - mCollider.r < 0 ) || ( mPosY + mCollider.r > SCREEN_HEIGHT ) || checkCollision( mCollider, square ) || checkCollision( mCollider, circle ) )
    {
        //Move back
        mPosY -= mVelY;
        shiftColliders();
    }
}

void Dot::move()
{
    //Move the dot left or right
    mPosX += mVelX;
    std::cout << "dot move" << ", mPosX=" << mPosX << ",mVelX=" << mVelX << std::endl;

    // 要检查圆点是否移出了水平面，而不是检查圆点是否移出了屏幕，因为屏幕会在水平面周围移动
    //If the dot went too far to the left or right
    if( ( mPosX < 0 ) || ( mPosX + DOT_WIDTH > LEVEL_WIDTH ) )
    {
        //Move back
        mPosX -= mVelX;
    }

    //Move the dot up or down
    mPosY += mVelY;
    std::cout << "dot move" << ", mPosY=" << mPosY << ",mVelY=" << mVelY << std::endl;

    //If the dot went too far up or down
    if( ( mPosY < 0 ) || ( mPosY + DOT_HEIGHT > LEVEL_HEIGHT ) )
    {
        //Move back
        mPosY -= mVelY;
    }
}

void Dot::shiftColliders()
{
    //行偏移量，每行碰状况高度不一致，所以需要变量记录当前行碰状况的y值
    // int r = 0;

    //查看点的碰撞框组
    // for( int set = 0; set < mColliders.size(); ++set )
    // {
    //     //将碰撞框居中
    //     mColliders[ set ].x = mPosX + ( DOT_WIDTH - mColliders[ set ].w ) / 2;

    //     //根据行偏移量设置碰撞框
    //     mColliders[ set ].y = mPosY + r;

    //     //将行偏移量向下移动碰撞框的高度
    //     r += mColliders[ set ].h;
    // }
    this->mCollider.x = this->mPosX;
    this->mCollider.y = this->mPosY;
}

std::vector<SDL_Rect>& Dot::getColliders()
{
    return mColliders;
}

Circle& Dot::getCollider()
{
    return mCollider;
}

void Dot::render()
{
    //Show the dot
    // gDotTexture.render( mPosX, mPosY );
    // 从 x 和 y 位置减去半径，将渲染位置偏移到圆的左上方
    gDotTexture.render( mPosX - mCollider.r, mPosY - mCollider.r );
}

// 将对象渲染到屏幕上时，会通过减去当前框坐标来渲染对象相对于当前框的位置
void Dot::render( int camX, int camY )
{
    //Show the dot relative to the camera
    gDotTexture.render( mPosX - camX, mPosY - camY );
}

bool init()
{
     //Initialization flag
    bool success = true;

    //Initialize SDL
    // 使用音乐和音效，因此需要在初始化视频的同时初始化音频
    if( SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO ) < 0 )
    {
        char str[1024];
        sprintf(str, "SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
        std::cout << str << std::endl;
        success = false;
    }
    else
    {
        //Create window
        gWindow = SDL_CreateWindow( "SDL Tutorial", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
        if( gWindow == nullptr )
        {
            char str[1024];
            sprintf(str, "Window could not be created! SDL_Error: %s\n", SDL_GetError());
            std::cout << str << std::endl;
            success = false;
        }
        else
        {
            //Create renderer for window
            // gRenderer = SDL_CreateRenderer( gWindow, -1, SDL_RENDERER_ACCELERATED );
            //为窗口创建垂直同步渲染器
            /*
            垂直同步,VSync 可以让渲染与显示器在垂直刷新时的更新同步进行。在本教程中，它将确保动画的运行速度不会太快。
            大多数显示器的运行速度约为每秒 60 帧，这也是我们的假设。如果你的显示器刷新率不同，这就能解释为什么动画运行得过快或过慢
            */
            gRenderer = SDL_CreateRenderer( gWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
            if( gRenderer == nullptr )
            {
                std::cout << "Renderer could not be created! SDL Error:" << SDL_GetError() << std::endl;
                success = false;
            }
            else
            {
                //Initialize renderer color
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );

                //Initialize PNG loading
                int imgFlags = IMG_INIT_PNG;
                if( !( IMG_Init( imgFlags ) & imgFlags ) )
                {
                    std::cout << "SDL_image could not initialize! SDL_image Error:" << IMG_GetError() << std::endl;
                    success = false;
                }

                //Initialize SDL_ttf
                if( TTF_Init() == -1 )
                {
                    std::cout << "SDL_ttf could not initialize! SDL_ttf Error:" << TTF_GetError() << std::endl;
                    success = false;
                }
            }

        }
    }

    return success;
}

bool loadMedia()
{
    //Loading success flag
    bool success = true;

    //Open the font
    //使用 TTF_OpenFont 加载字体。这需要输入字体文件的路径和我们要渲染的点尺寸
    std::cout << "load media gFont " << std::endl;
    gFont = TTF_OpenFont( "lazy.ttf", 28 );
    if( gFont == nullptr )
    {
        std::cout << "Failed to load lazy font! SDL_ttf Error:" << TTF_GetError() << std::endl;
        success = false;
    }
    // if( !gDotTexture.loadFromFile( "dot.bmp" ) )
    // {
    //     std::cout << "Failed to load dot texture image!" << std::endl;
    //     success = false;
    // }
    else
    {
        /*
        尽量减少渲染文本的次数。使用一个纹理来提示输入，并使用一个纹理以毫秒为单位显示当前时间。时间纹理每帧都会改变，因此必须每帧都渲染一次，
        但提示纹理不会改变，因此可以在文件加载函数中渲染一次
        */
        if( !gPromptTexture.loadFromRenderedText( "Select your recording device:", textColor ) )
        {
            std::cout << "Failed to render text texture!" << std::endl;
            success = false;
        }
        //使用 SDL_GetNumAudioDevices 获取可用录音设备的数量,当输入 SDL_TRUE 参数时，它将提供录音设备的数量,如果输入的是 SDL_FALSE，则会显示重放设备的数量
        gRecordingDeviceCount = SDL_GetNumAudioDevices( SDL_TRUE );

        //No recording devices
        if( gRecordingDeviceCount < 1 )
        {
            printf( "Unable to get audio capture device! SDL Error: %s\n", SDL_GetError() );
            success = false;
        }

         //At least one device connected
        else
        {
            //如果连接了录音设备，将使用的设备数量限制在 10 个
            if( gRecordingDeviceCount > MAX_RECORDING_DEVICES )
            {
                gRecordingDeviceCount = MAX_RECORDING_DEVICES;
            }

            //将这些设备的名称渲染屏幕
            std::stringstream promptText;
            for( int i = 0; i < gRecordingDeviceCount; ++i )
            {
                //使用 SDL_GetAudioDeviceName 获取设备名称，并通过 SDL_TRUE 和录音设备的索引输入想要的录音设备名称
                promptText.str( "" );
                std::string temp = SDL_GetAudioDeviceName( i, SDL_TRUE );
                promptText << i << ": " << temp;
                std::cout << i << ":" << temp << std::endl;

                //Set texture from name
                gDeviceTextures[ i ].loadFromRenderedText( promptText.str().c_str(), textColor );
            }
        }
    }

    return success;
}

void close()
{
    gDotTexture.free();
    gPromptTexture.free();
    for (int i = 0; i < MAX_RECORDING_DEVICES; i++)
    {
        gDeviceTextures[i].free();
    }
    

    //Free global font
    TTF_CloseFont( gFont );
    gFont = nullptr;

    //Destroy window
    SDL_DestroyRenderer( gRenderer );
    SDL_DestroyWindow( gWindow );
    gRenderer = nullptr;
    gWindow = nullptr;

    //Free playback audio
    if( gRecordingBuffer != nullptr )
    {
        delete[] gRecordingBuffer;
        gRecordingBuffer = nullptr;
    }

    //Quit SDL subsystems
    TTF_Quit();
    Mix_Quit();
    IMG_Quit();
    SDL_Quit();
}

/*
当录音设备未被暂停时，它会开始定期调用给它的录音回调。
函数只是将设备流中的字节复制到录音缓冲区的当前位置，然后移动缓冲区中的位置。
这就是录音的全部内容，只是抓取音频数据块。请记住，"len "是以字节为单位的数据流块大小
*/
void audioRecordingCallback( void* userdata, Uint8* stream, int len )
{
    //Copy audio from stream
    memcpy( &gRecordingBuffer[ gBufferBytePosition ], stream, len );

    //Move along buffer
    gBufferBytePosition += len;
}

//回放回调也与录制回调类似。这里的主要区别在于，不是将数据从设备复制到缓冲区，而是将记录在缓冲区的数据复制回设备。
void audioPlaybackCallback( void* userdata, Uint8* stream, int len )
{
    //Copy audio to stream
    memcpy( stream, &gRecordingBuffer[ gBufferBytePosition ], len );

    //Move along buffer
    gBufferBytePosition += len;
}

int main( int argc, char* args[] )
{
    //Start up SDL and create window
    if( !init() )
    {
        std::cout << "Failed to initialize!" << std::endl;
    }
    else
    {
        //Load media
        if( !loadMedia() )
        {
            std::cout << "Failed to load media!" << std::endl;
        }
        else
        {
            //Hack to get window to stay up
            SDL_Event e; 
            bool quit = false; 

            //设置默认录音状态
            RecordingState currentState = SELECTING_DEVICE;

            //音频设备 ID
            SDL_AudioDeviceID recordingDeviceId = 0;
            SDL_AudioDeviceID playbackDeviceId = 0;

            //While application is running
            while( !quit )
            {
                //Handle events on queue
                while( SDL_PollEvent( &e ) != 0 )
                {
                    //User requests quit
                    if( e.type == SDL_QUIT )
                    {
                        quit = true;
                    }
                    //Do current state event handling
                    switch( currentState )
                    {
                        //User is selecting recording device
                        case SELECTING_DEVICE:

                            //On key press
                            if( e.type == SDL_KEYDOWN )
                            {
                                //因为 SDLK 常量是连续的，可以通过用 SDLK_0 减去 keysym 来进行转换
                                if( e.key.keysym.sym >= SDLK_0 && e.key.keysym.sym <= SDLK_9 )
                                {
                                    //Get selection index
                                    int index = e.key.keysym.sym - SDLK_0;

                                    //如果按下了有效的索引键，指定录音音频规格
                                    if( index < gRecordingDeviceCount )
                                    {
                                        //Default audio spec
                                        SDL_AudioSpec desiredRecordingSpec;
                                        // 用 SDL_zero 初始化音频规格。在使用内存之前，一定要先初始化内存
                                        SDL_zero(desiredRecordingSpec);
                                        // 将频率设置为 44.1 khz，这是 CD 音质。数据使用 32 位浮点格式。设置 2 个通道，因为要立体声。采样率设置为 4096，这是一个相当标准的大小
                                        desiredRecordingSpec.freq = 44100;
                                        desiredRecordingSpec.format = AUDIO_F32;
                                        desiredRecordingSpec.channels = 2;
                                        desiredRecordingSpec.samples = 4096;
                                        // 提供音频录制回调
                                        desiredRecordingSpec.callback = audioRecordingCallback;

                                        /*调用 SDL_OpenAudioDevice，并传入录音设备名称、想要的 SDL_TRUE 录音设备、想要的规格、从驱动程序返回的规格指针，
                                        最后还有一个标志，表示可以接受 SDL_OpenAudioDevice 提供与要求不同的格式。
                                        */
                                        recordingDeviceId = SDL_OpenAudioDevice( SDL_GetAudioDeviceName( index, SDL_TRUE ), SDL_TRUE, &desiredRecordingSpec, &gReceivedRecordingSpec, SDL_AUDIO_ALLOW_FORMAT_CHANGE );
                                        
                                        //设备打开失败
                                        if( recordingDeviceId == 0 )
                                        {
                                            //Report error
                                            std::cout << "Failed to open recording device! SDL Error:" << SDL_GetError() << std::endl;
                                            gPromptTexture.loadFromRenderedText( "Failed to open recording device!", textColor );
                                            currentState = ERROR;
                                        }
                                        //打开成功会创建一个与录制规范基本相同的回放规范。主要区别在于它使用的是回放回调而不是录制回调
                                        else
                                        {
                                            //Default audio spec
                                            SDL_AudioSpec desiredPlaybackSpec;
                                            SDL_zero(desiredPlaybackSpec);
                                            desiredPlaybackSpec.freq = 44100;
                                            desiredPlaybackSpec.format = AUDIO_F32;
                                            desiredPlaybackSpec.channels = 2;
                                            desiredPlaybackSpec.samples = 4096;
                                            desiredPlaybackSpec.callback = audioPlaybackCallback;

                                            //打开重放设备的过程基本相同。在本教程中，不关心获得哪个回放设备，因此通过 nullptr 来获取第一个可用的设备。在第二个参数中输入 SDL_FALSE，以打开重放设备而不是录制设备
                                            playbackDeviceId = SDL_OpenAudioDevice( nullptr, SDL_FALSE, &desiredPlaybackSpec, &gReceivedPlaybackSpec, SDL_AUDIO_ALLOW_FORMAT_CHANGE );

                                             //设备打开失败
                                            if( playbackDeviceId == 0 )
                                            {
                                                //Report error
                                                std::cout << "Failed to open playback device! SDL Error:" << SDL_GetError() << std::endl;
                                                gPromptTexture.loadFromRenderedText( "Failed to open playback device!", textColor );
                                                currentState = ERROR;
                                            }
                                            //如果设备成功打开，创建一个字节缓冲区，用于保存要录制和回放的音频数据
                                            else
                                            {
                                                //计算每个采样的字节数
                                                //如果有 2 个通道，每个通道采样 32 位（使用音频格式上的 SDL_AUDIO_BITSIZE），那么得到 2 个通道 * ( 32 位 / 每字节 8 位 )，即每个采样 8 个字节。
                                                int bytesPerSample = gReceivedRecordingSpec.channels * ( SDL_AUDIO_BITSIZE( gReceivedRecordingSpec.format ) / 8 );

                                                //计算每秒字节数
                                                //将每采样字节数乘以频率，即每秒的采样数。每个采样 8 字节 * 每秒 44100 个采样，即每秒 705600 字节
                                                int bytesPerSecond = gReceivedRecordingSpec.freq * bytesPerSample;

                                                //计算缓冲区大小
                                                /*希望有 6 秒钟的缓冲时间（5 秒钟 + 1 秒钟的填充时间），因此将缓冲区大小设置为 4233600 字节。
                                                这看起来很多，但其实也就 4 兆多一点。请记住，由于最大位置的原因，只使用了 6 秒缓冲区中的 5 秒
                                                */
                                                gBufferByteSize = RECORDING_BUFFER_SECONDS * bytesPerSecond;

                                                //计算缓冲区最大使用量
                                                gBufferByteMaxPosition = MAX_RECORDING_SECONDS * bytesPerSecond;

                                                //分配和初始化字节缓冲区
                                                gRecordingBuffer = new Uint8[ gBufferByteSize ];
                                                memset( gRecordingBuffer, 0, gBufferByteSize );

                                                //进入下一个状态
                                                gPromptTexture.loadFromRenderedText("Press 1 to record for 5 seconds.", textColor);
                                                currentState = STOPPED;
                                            }
                                        }
                                    }
                                }
                            }
                            break;    

                        //分配完缓冲区后，可以开始录音了
                        case STOPPED:

                            //On key press
                            if( e.type == SDL_KEYDOWN )
                            {
                                /*如果用户按下 "1"，将缓冲区位置设置回 "0"，并使用 SDL_PauseAudioDevice 取消暂停音频设备。
                                第一个参数是要暂停/取消暂停的设备，第二个参数决定要暂停还是取消暂停。传递 SDL_FALSE 将取消暂停设备

                                音频设备默认为暂停状态，这意味着在取消暂停之前，它们不会录制或播放
                                */
                                if( e.key.keysym.sym == SDLK_1 )
                                {
                                    //Go back to beginning of buffer
                                    gBufferBytePosition = 0;

                                    //Start recording
                                    SDL_PauseAudioDevice( recordingDeviceId, SDL_FALSE );

                                    //Go on to next state
                                    gPromptTexture.loadFromRenderedText( "Recording...", textColor );
                                    currentState = RECORDING;
                                }
                            }
                            break;   
                        //User has finished recording
                        case RECORDED:
                            //On key press
                            if( e.type == SDL_KEYDOWN )
                            {
                                //记录 5 秒钟后的事件处理。与开始录制时的情况类似。将缓冲区位置设置回起始位置，取消播放设备的暂停，并设置下一个状态
                                if( e.key.keysym.sym == SDLK_1 )
                                {
                                    //Go back to beginning of buffer
                                    gBufferBytePosition = 0;

                                    //Start playback
                                    SDL_PauseAudioDevice( playbackDeviceId, SDL_FALSE );

                                    //Go on to next state
                                    gPromptTexture.loadFromRenderedText( "Playing...", textColor );
                                    currentState = PLAYBACK;
                                }
                                 //回到用户录制后的事件处理，允许用户重新录制。当想重新录制时，只需将缓冲区位置跳回到起始位置，初始化缓冲区，并取消录制设备的暂停
                                if( e.key.keysym.sym == SDLK_2 )
                                {
                                    //Reset the buffer
                                    gBufferBytePosition = 0;
                                    memset( gRecordingBuffer, 0, gBufferByteSize );

                                    //Start recording
                                    SDL_PauseAudioDevice( recordingDeviceId, SDL_FALSE );

                                    //Go on to next state
                                    gPromptTexture.loadFromRenderedText( "Recording...", textColor );
                                    currentState = RECORDING;
                                }
                            }
                            break;
                    }
                }
                //录音时，需要检查是否已填满 5 秒钟的缓冲区
				if( currentState == RECORDING )
				{
					/*在检查缓冲区位置之前，必须调用 SDL_LockAudioDevice。
                    问题是，回调正在另一个线程中运行，不想让两个线程同时访问同一个变量。
                    SDL_LockAudioDevice 可以阻止回调被调用，同时还需要访问回调所操作的缓冲区位置。
                    */
					SDL_LockAudioDevice( recordingDeviceId );

					/*
                    一旦录音设备被锁定，会检查缓冲区位置是否已超过 5 秒钟的数据量。如果是，将暂停记录设备以停止记录，
                    然后进入下一个状态。最后，调用 SDL_UnlockAudioDevice，这样如果仍有数据要录制，录音设备就可以继续
                    */
					if( gBufferBytePosition > gBufferByteMaxPosition )
					{
						//Stop recording audio
						SDL_PauseAudioDevice( recordingDeviceId, SDL_TRUE );

						//Go on to next state
						gPromptTexture.loadFromRenderedText( "Press 1 to play back. Press 2 to record again.", textColor );
						currentState = RECORDED;
					}

					//Unlock callback
					SDL_UnlockAudioDevice( recordingDeviceId );
				}
				//更新重放与录制同样类似。锁定重放设备，检查重放位置，如果缓冲区位置超过终点，则停止重放，然后解锁重放设备。
				else if( currentState == PLAYBACK )
				{
					//Lock callback
					SDL_LockAudioDevice( playbackDeviceId );

					//Finished playback
					if( gBufferBytePosition > gBufferByteMaxPosition )
					{
						//Stop playing audio
						SDL_PauseAudioDevice( playbackDeviceId, SDL_TRUE );

						//Go on to next state
						gPromptTexture.loadFromRenderedText( "Press 1 to play back. Press 2 to record again.", textColor );
						currentState = RECORDED;
					}

					//Unlock callback
					SDL_UnlockAudioDevice( playbackDeviceId );
				}

				//Clear screen
				SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
				SDL_RenderClear( gRenderer );

				//Render prompt centered at the top of the screen
				gPromptTexture.render( ( SCREEN_WIDTH - gPromptTexture.getWidth() ) / 2, 0 );

				//User is selecting 
				if( currentState == SELECTING_DEVICE )
				{
					//Render device names
					int yOffset = gPromptTexture.getHeight() * 2;
					for( int i = 0; i < gRecordingDeviceCount; ++i )
					{
						gDeviceTextures[ i ].render( 0, yOffset );
						yOffset += gDeviceTextures[ i ].getHeight() + 1;
					}
				}

				//Update screen
				SDL_RenderPresent( gRenderer );
            }
        }
    }

    //Free resources and close SDL
    close();

    return 0;
}



